# UI app for MuslBuilder
require 'rubygems'
require 'wx'
require 'erb'
require 'common/base'
Dir.glob("components/*/*.{rb}").each {|c| require c}
require 'muslbuilder'

$DEBUG = false

# include MuslMessage

class Builder < Wx::Frame

    EVT_KEY_UP = 315
    EVT_KEY_DOWN = 317
    MENU_FLOWS = 1000
    MENU_PREVIEW = 2000
    RUBY_FILE_TYPES = "Ruby files (*.rb)|*.rb"
    MSL_FILE_TYPES = "MuSL files (*.msl)|*.msl"

    def initialize()
        super(nil, -1, "MuSL Builder", [400,200], [600,640] )

        @properties = []
        @parameters = []
        @selected_messages = []
        @component_name = nil
        @component_base = nil
        @components = {}  # maps names to base classes
        @output_file = "builder.msl"

        evt_close {|event| on_close(event)}

        # GUI ELEMENTS
        
        @help_widget = Wx::StaticText.new(
           self,                  #Parent
           :label => "1. Select Component(s), Click 'Ok'\n2. Fill in Parameters (if any)\n3. Add Messages to Scenario Steps\n4. Set Output File name\n5. Click 'Build MuSL'",
           :pos => [20, 10],
           :size => Wx::DEFAULT_SIZE
        )

        @component_widget = Wx::StaticText.new(
           self,                  #Parent
           :label => "",
           :pos => [30, 130],
           :size => Wx::DEFAULT_SIZE
        )

        @component_selection_list_box = Wx::ListBox.new(self, -1,  [10, 100], [130,140], nil, Wx::LB_MULTIPLE|Wx::LB_NEEDED_SB)
        component_select_button = Wx::Button.new(self, -1, "Ok", [145, 100])
        evt_button(component_select_button.get_id()) {|event| on_component_select_button(event)}

        clear_button = Wx::Button.new(self, -1, "Clear", [160, 220])
        evt_button(clear_button.get_id()) {|event| clear_all}

        run_button = Wx::Button.new(self, -1, "Build MuSL", [490, 500])
        evt_button(run_button.get_id()) {|event| build_musl}

        quit_button = Wx::Button.new(self, -1, "Quit", [500, 540])
        evt_button(quit_button.get_id()) {|event| on_quit_button(event)}

        @params_label = Wx::StaticText.new(
           self,                  #Parent
           :label => "Parameters",
           :pos => [350, 10],
           :size => Wx::DEFAULT_SIZE
        )

        @params_widget = Wx::TextCtrl.new(
           self,                  #Parent
           -2,
           :value => "",
           :pos => [250, 30],
           :size => [340, 210],
           :style => Wx::TE_RICH2|Wx::TE_WORDWRAP|Wx::TE_MULTILINE
        )

       @output_file_label = Wx::StaticText.new(
           self,                  #Parent
           :label => "Output File:",
           :pos => [180, 54],
           :size => Wx::DEFAULT_SIZE
        )

        # set to empty string for output to stdout
        @output_file_widget = Wx::TextCtrl.new(
           self,                  #Parent
           -2,
           :value => "output_filename",
           :pos => [260, 540],
           :size => [200, 20],
           :style => Wx::TE_RICH2|Wx::TE_WORDWRAP|Wx::TE_MULTILINE
        )

        @scenario_name_label = Wx::StaticText.new(
           self,                  #Parent
           :label => "Scenario Name:",
           :pos => [160, 570],
           :size => Wx::DEFAULT_SIZE
        )

        # set to empty string for output to stdout
        @scenario_name_widget = Wx::TextCtrl.new(
           self,                  #Parent
           -2,
           :value => "default_scenario_name",
           :pos => [260, 570],
           :size => [200, 20],
           :style => Wx::TE_RICH2|Wx::TE_WORDWRAP|Wx::TE_MULTILINE
        )

        @message_options_widget = Wx::StaticText.new(
           self,                  #Parent
           :label => "Available Messages",
           :pos => [80, 255],
           :size => Wx::DEFAULT_SIZE
        )

        @message_options_list_box = Wx::ListBox.new(self, -1,  [10,270], [290,200], nil, Wx::LB_SINGLE|Wx::LB_NEEDED_SB|Wx::LB_HSCROLL)
        # evt_listbox(@message_options_list_box.get_id()) {|event| on_list_select(event)}


        @message_selections_widget = Wx::StaticText.new(
           self,                  #Parent
           :label => "Scenario Steps",
           :pos => [370, 255],
           :size => Wx::DEFAULT_SIZE
        )


        @message_selections_list_box = Wx::ListBox.new(self, -1,  [305,270], [290,200], nil, Wx::LB_SINGLE|Wx::LB_NEEDED_SB|Wx::LB_HSCROLL)
        evt_listbox(@message_selections_list_box.get_id()) {|event| on_list_select(event)}
        @message_selections_list_box.evt_key_down    { |evt| on_message_selections_list_box_key_down(evt) }

        add_message_button = Wx::Button.new(self, -1, "Add", [110, 480])
        evt_button(add_message_button.get_id()) {|event| on_add_message_button(event)}

        remove_message_button = Wx::Button.new(self, -1, "Remove", [340, 480])
        evt_button(remove_message_button.get_id()) {|event| on_remove_message_button(event)}

        # transport control to be populated by @transport_list
        transport_label = Wx::StaticText.new(self, :label => "Transport:", :pos => [40, 510],:size => Wx::DEFAULT_SIZE)
        @transport_control = Wx::Choice.new(self, Wx::ID_ANY, [110, 510], [100, 20], nil)

        # MENU
        menu_options = Wx::Menu.new()
        menu_options.append(MENU_FLOWS, "&Flows...\t", 'Options')
        menu_options.append(MENU_PREVIEW, "&Preview...\t", 'Options')

        menu_bar  = Wx::MenuBar.new()
        menu_bar.append(menu_options, "&Options")

        set_menu_bar( menu_bar )
        evt_menu(MENU_FLOWS) { | e | on_flows_menu(e) }
        evt_menu(MENU_PREVIEW) { | e | on_preview_menu(e) }

        # POPULATE LISTS
        populate_component_list()

        # SHOW FRAME
        show()
    end

    def on_close(event)
        destroy()
    end

    def on_quit_button(event)
       destroy()
    end

    def clear_all
       num = @component_selection_list_box.get_count
       num.times do | i |
         @component_selection_list_box.deselect(i)
       end
       @params_widget.clear
       @message_options_list_box.clear
       @message_selections_list_box.clear
    end

    # TODO: Called with currently selected values
    # flows window may end up making its own muslbuilder call ...
    # by doing what on_run_button does in this class
    def on_flows_menu(event)
       # open the flow selector
      available_messages = []
      count = @message_options_list_box.get_count
      count.times do | i |
        available_messages << @message_options_list_box.get_string(i)
      end
      
      params = {}
      params["components"] = @components  # currently selected component map
      params["messages"] = available_messages  # @selected_messages # all available for messages for selected components
      params["transports"] = get_transports # extracted from currently selected component base classes
      params["hosts"] = ["A1", "A2"] #  TODO: @components + hosts ??
      params["parameters"] = @params_widget.value  # current state of the parameters window
      flows = FlowsDialog.new(self, "Flows", params)
      result = flows.show_modal()
      if result == 0 # Wx::ID_OK
        @selected_messages = flows.selected_messages
        @selected_messages.each do | m |
          puts m
        end
      end
    end

    def on_preview_menu(event)
       # open a file dialog to select a msl filed
      filename = ""
      file = Wx::FileDialog.new( nil, "Choose a msl file", "", "", MSL_FILE_TYPES,
                               Wx::OPEN|Wx::CHANGE_DIR|Wx::FILE_MUST_EXIST )
      case file.show_modal()
       when Wx::ID_OK
         filename = file.get_directory + "/" + file.get_filename
       when Wx::ID_CANCEL
         $DEBUG and puts "NOT OK"
      end
       # open a modeless dialog with a text area
       # displaying the selected msl file
       
       pd = PreviewDialog.new(self, "#{file.get_filename}", filename)
       pd.show
    end

    # assumes component base classes have newlines between each transport in their template
    # and splits on the equals sign, gets the value on the left (stripped of white space)
    def get_transports
      arr = []
      @components.each do | k, v |
         base = eval(v).new(nil, nil)
         transports = base.base_transports
         t = transports.split("\n")
         t.each do | x |
           p = x.split("=")
           arr << p[0].strip
         end
      end
      return arr
    end

    def populate_component_list
      arr = []
      Dir.glob("components/*").each do |c|
        cmp = c[c.rindex("/") + 1, c.length]
        arr << cmp
      end
      @component_selection_list_box.set(arr)
    end

   def populate_transport_choice
     @transport_list = get_transports
     @transport_list.insert(0, "Default")
     @transport_control.clear
     @transport_list.each do | t |
       @transport_control.append(t)
     end
     @transport_control.set_selection(0)
    end
    
    def on_component_select_button(event)
     @selected_messages.clear
     @message_options_list_box.clear
     @message_selections_list_box.set(@selected_messages)
     @parameters = []
     @components = {}
     @params_widget.clear

     @component_selection_list_box.each do | i |
      if @component_selection_list_box.is_selected(i)     
        @component_name = @component_selection_list_box.string(i)
        @component_base = "#{@component_name}Base"
        @components[@component_name] = @component_base
        # append param options
        show_params
      end
     end
    # display message options
    show_message_options
    populate_transport_choice
    end

    # handles only one move at a time
    def on_message_selections_list_box_key_down(event)
      if (event.get_key_code == EVT_KEY_UP)
        @message_selections_list_box.each do | i |
          if @message_selections_list_box.is_selected(i) and i > 0
            tmp = @selected_messages[i-1]
            @selected_messages[i-1] = @message_selections_list_box.string(i)
            @selected_messages[i] = tmp
            @message_selections_list_box.set(@selected_messages)
            break
          end
        end
      elsif (event.get_key_code == EVT_KEY_DOWN)
        @message_selections_list_box.each do | i |
          if @message_selections_list_box.is_selected(i) and i < @selected_messages.length - 1
            tmp = @selected_messages[i+1]
            @selected_messages[i+1] = @message_selections_list_box.string(i)
            @selected_messages[i] = tmp
            @message_selections_list_box.set(@selected_messages)
            break
          end
        end
      end
    end

    def on_list_select(event)
     # nothing to do. something has been selected but we don't care.
     # the remove button or key event handlers will will deal with it
    end

    # generate msl file using musl_builder and properties array
    def build_musl()
      # add the messages string to the properties array
      str = "messages="
      @message_selections_list_box.each do | i |
        str = str +  @message_selections_list_box.string(i) + ","
      end
      str = str[0, str.length-1]
      @properties << str

      # add the parameters strings to the properties array
      # TODO: assumes new line between each param. may not be the case always ?
      @parameters = []
      results = @params_widget.get_value
      params = results.split("\n")
      params.each do | p |
         @parameters << p.chomp
      end
      @parameters.each do | p |
        @properties << p
      end

      # add the components to the properties array
      cstr = "components="
      @components.each do | k, v |
         cstr = cstr + k + ","
      end
      cstr = cstr[0, cstr.length - 1] # lose the final comma
      @properties << cstr

      # add the output_file to the properties array
      text = @output_file_widget.value
      @properties << "output_file=#{text}.msl"

      # add the scenario name to the properties array
      scenario_name = @scenario_name_widget.value
      @properties << "scenario_name=#{scenario_name}"

      # output a copy of the properties file to disk for reuse
      File.open("#{text}.properties", 'w') do |f2|
         @properties.each do | p |
            f2.puts p
         end
      end

      # invoke muslbuilder with the properties array and build the msl file
      $msb = MuSLBuilder.new(@properties)
      output = $msb.output
      $DEBUG and puts output
    end

    # single items selectable, added one at a time to selected messages
    def on_add_message_button(event)
      sel = @message_options_list_box.get_string_selection
      # append the transport if not Default
      transport = @transport_control.get_string_selection
      if transport != "Default"
        sel = sel + "[" + transport + "]"
      end
      # add to selected messages array
      @selected_messages << sel
      @message_selections_list_box.set(@selected_messages)
    end

    # delete the item from the selected messages array and re-post it
    # multiple items selectable
    def on_remove_message_button(event)
      @message_selections_list_box.each do | i |
        if @message_selections_list_box.is_selected(i)
          @selected_messages.delete_at(i)
          @message_selections_list_box.set(@selected_messages)
        end
      end      
    end

    # use introspection to list the message classes of the selected component
    def show_message_options
      bb = []
        @components.each do | k, v |
          Dir.glob("components/#{k}/*.txt").each do | c |
            str = c[c.rindex("/") + 1, c.length]
            str = str[0, str.length - 4] # lose the .txt suffix
            bb << str
         # end
        end
      end # constants each do
      @message_options_list_box.insert_items(bb, 0)
    end

    def show_params
      unless @component_base.nil?
        @parameters = []
        arr = []
        # arr << "output_file"  # TODO: put putput_file option set somewhere else
        excludes = ["@step", "@transport", "@message", "@base_options", "@base_variables", "@base_transports", "@global_base"]
        base_class = Kernel.const_get(@component_base).new(0, nil, {})
        base_class.instance_variables.each do |v|
          if !excludes.include?(v)
            arr << v[1, v.length]
          end
        end

        str = ""
        arr.each do | p |
          str = str + p + "=\n"
        end

        @params_widget.append_text(str)
      end # unless
    end

    def message(text, title)
      m = Wx::MessageDialog.new(self, text, title, Wx::OK | Wx::ICON_INFORMATION)
      m.show_modal()
    end

    def get_file_as_string_array(filename)
     arr = []
     f = File.open(filename, "r")
     f.each_line do |line|
       arr.push line
     end
     return arr
    end
end

  ## --------------------------- PreviewDialog ----------------------------

class PreviewDialog < Wx::Dialog
 attr_accessor :value
 def initialize(parent, title, filename)
   super(parent, Wx::ID_ANY, title, [500,300], [500, 500], Wx::RESIZE_BORDER|Wx::CLOSE_BOX )
   
   str = ""
   f = File.open(filename, "r")
   f.each_line do |line|
     str = str + line
   end

   w = Wx::TextCtrl.new(
           self,                 
           id,
           :value => str,
           :pos => [10, 20],
           :size => [480, 400],
           :style => Wx::TE_RICH2|Wx::TE_WORDWRAP|Wx::TE_MULTILINE
        )

   ok_button = Wx::Button.new(self, -1, "OK", [400, 450])
   evt_button(ok_button.get_id()) {|event| on_ok_button(event)}
 end

 def on_ok_button(event)
   destroy()
 end

end

 ## --------------------------- FlowsDialog ----------------------------

# TODO: on constructing message list to pass to muslbuilder,
# append [transport] to each selected message name
# message = @steps["message"] + "[" + @steps["transport"] + "]"

# TODO: get focus off of text control to begin with! set focus to nothing if possible

# need to be able to
#    done! delete rows from the grid -  triple-click to select row, press delete key
#    done! re-arrange rows from the grid - triple-click to select row, then up/down arrow to move it
#    format @steps for properties (Message[Transport])
#    call muslbuilder from here

class FlowsDialog < Wx::Dialog

 EVT_KEY_UP = 315
 EVT_KEY_DOWN = 317
 EVT_DELETE_KEY = 8

 attr_accessor :value, :selected_messages
 
 def initialize(parent, title, params={})
   super(parent, Wx::ID_ANY, title, [600,200], [500, 600] )
   @selected_messages = []
   @hosts = []
   @components_map = params["components"] # hashmap of selected component names and their base classes
   @message_list = params["messages"]  # expecting an array of message names
   @transport_list = params["transports"] # expecting an array of transport names
   @parameters = params["parameters"] # passthrough to muslbuilder
   @host_list = params["hosts"] # expecting an array of host names
   @steps = []  # built by the actions in this window
   @selected_messages = []  # final state for muslbuilder

   evt_key_down{ |evt| on_grid_select(evt) }

   if @message_list.length == 0
     md = Wx::MessageDialog.new(self, "Sorry, there are no messages to work with\n\nSelect one or more components from the main window and try again", "Hold on there a sec!", Wx::OK|Wx::ICON_EXCLAMATION)
     md.show_modal
     destroy
     return
   end

   @host_textfield = Wx::TextCtrl.new(
           self,
           id,
           :value => "",
           :pos => [10, 20],
           :size => [260, 20],
           :style => Wx::TE_RICH2|Wx::TE_WORDWRAP|Wx::TE_MULTILINE
        )

   # src control to be populated by hosts list
   src_label = Wx::StaticText.new(self, :label => "src", :pos => [10, 60],:size => Wx::DEFAULT_SIZE)
   @src_control = Wx::Choice.new(self, Wx::ID_ANY, [70, 60], [200, 40], nil)
   evt_choice(@src_control.get_id()) {|event| on_src_select(event)}

   # dst control to be populated by hosts list
   dst_label = Wx::StaticText.new(self, :label => "dst", :pos => [10, 100],:size => Wx::DEFAULT_SIZE)
   @dst_control = Wx::Choice.new(self, Wx::ID_ANY, [70, 100], [200, 40], nil)
   evt_choice(@dst_control.get_id()) {|event| on_dst_select(event)}
   populate_host_choice  # TODO: demo only, renove when real hosts exist

   # transport control to be populated by @transport_list
   transport_label = Wx::StaticText.new(self, :label => "transport", :pos => [10, 140],:size => Wx::DEFAULT_SIZE)
   @transport_control = Wx::Choice.new(self, Wx::ID_ANY, [70, 140], [200, 40], nil)
   evt_choice(@transport_control.get_id()) {|event| on_transport_select(event)}
   populate_transport_choice

   # message control to be populated by @message_list
   message_label = Wx::StaticText.new(self, :label => "message", :pos => [10, 180],:size => Wx::DEFAULT_SIZE)
   @message_control = Wx::Choice.new(self, Wx::ID_ANY, [70, 180], [300, 40], nil)
   evt_choice(@message_control.get_id()) {|event| on_message_select(event)}
   populate_message_choice

   add_step_button = Wx::Button.new(self, -1, "Add Step", [400, 180])
   evt_button(add_step_button.get_id()) {|event| on_add_step_button(event)}

   add_host_button = Wx::Button.new(self, -1, "Add Host", [300, 20])
   evt_button(add_host_button.get_id()) {|event| on_add_host_button(event)}

   ok_button = Wx::Button.new(self, -1, "OK", [425, 540])
   evt_button(ok_button.get_id()) {|event| on_ok_button(event)}
   
   create_grid
 end

 # called on all changes to re-create and re-display
 def create_grid
   @grid = Wx::Grid.new(self, -1, [20,260], [400, 300] )
   @grid.enable_drag_col_size(true)
   @grid.enable_drag_col_move()
   # evt_grid_range_select() { | event | on_grid_select(event) }
   hosts_len = @host_list.length
   @grid.create_grid(@steps.length, hosts_len)
   hosts_len.times do | i |     
      @grid.set_col_label_value(i, @host_list[i])
   end

   if @steps.length > 0
     # populate table values with @steps
     row = 0
     @steps.each do | step |
       src_col = -1
       dst_col = -1
       src = step["src"]
       dst = step["dst"]

       # match column names with src and dst
       hosts_len.times do | i |
          if @host_list[i] == src
            src_col = i
          elsif @host_list[i] == dst
            dst_col = i
          end
       end

       transport = step["transport"] 
       message = step["message"]

       @grid.set_cell_value(row, src_col, message)
       @grid.set_cell_value(row, dst_col, "dst")
       row = row + 1
     end # @steps.each
   end
 end

 # triple-click to select row, then up/down arrow to move it
 def on_grid_select(event)
    arr = @grid.get_selected_rows
    return if arr.length == 0
    num_cols = @grid.get_number_cols
    num_rows = @grid.get_number_rows
    sel_row = arr[0]  # just do the first one if multiple select. ignore the others


    # delete the selected row or swap the selected row's columns with the one above or below
    tmp = ''
    if (event.get_key_code == EVT_DELETE_KEY)
      @grid.delete_rows(sel_row, 1)
      @steps.delete_at(sel_row)
    elsif (event.get_key_code == EVT_KEY_UP) and sel_row > 0
      # save the row above
      num_cols.times do | i |
        tmp = @grid.get_cell_value(sel_row - 1, i)
        new_val = @grid.get_cell_value(sel_row, i)
        @grid.set_cell_value(sel_row - 1, i, new_val)
        @grid.set_cell_value(sel_row, i, tmp)
      end
    elsif (event.get_key_code == EVT_KEY_DOWN) and sel_row < num_rows - 1
      # save the row below
      num_cols.times do | i |
        tmp << @grid.get_cell_value(sel_row + 1, i)
        new_val = @grid.get_cell_value(sel_row, i)
        @grid.set_cell_value(sel_row + 1, i, new_val)
        @grid.set_cell_value(sel_row , i, tmp)
      end
    end
 end

 def on_ok_button(event)
   build_message_list # sets @selected_messages for return value
   destroy()
 end

 def on_add_host_button(event)
   puts "add_host"
   host = @host_textfield.value
   @src_control.append(host)
   @dst_control.append(host)
   @host_textfield.clear
   @src_control.set_selection(0)
   @dst_control.set_selection(0)
   @host_list << host
 end

 def on_add_step_button(event)
   #src and dst must be different or else reject it
   if @src_control.get_string_selection == @dst_control.get_string_selection
     # show alert box
     md = Wx::MessageDialog.new(self, "The src and dst hosts must not be the same", "Hold on there a sec!", Wx::OK|Wx::ICON_EXCLAMATION)
     md.show_modal
     return
   end
   
   step = {}
   step["src"] = @src_control.get_string_selection
   step["dst"] = @dst_control.get_string_selection
   step["transport"] = @transport_control.get_string_selection
   step["message"] = @message_control.get_string_selection
   @steps << step
   create_grid
 end

 def on_src_select(event)
 #  puts "src_select"
 end

 def on_dst_select(event)
  # puts "dst_select"
 end

 def on_transport_select(event)
  # puts "transport_select"
 end

 def on_message_select(event)
  # puts "message_select"
 end

 def populate_transport_choice
   @transport_control.clear
   @transport_list.each do | t |
     @transport_control.append(t)
   end
   @transport_control.set_selection(0)
 end

 def populate_message_choice
   @message_control.clear
   @message_list.each do | t |
     @message_control.append(t)
   end
   @message_control.set_selection(0)
 end

 # expecting two host minumum
 def populate_host_choice
   @src_control.clear
   @dst_control.clear
   @host_list.each do | t |
     @src_control.append(t)
     @dst_control.append(t)
   end
   @src_control.set_selection(0)
   @dst_control.set_selection(1)
 end

 # go through the grid row by row, find the transport and the message name
 def build_message_list
   @selected_messages = []
   num_rows = @grid.get_number_rows
   num_cols = @grid.get_number_cols
   num_rows.times do | r |
      num_cols.times do | c |
        val = @grid.get_cell_value(r, c)
        if val != "" and val != "dst"
          transport = @grid.get_col_label_value(c)
          message = "#{val}[#{transport}]"
          @selected_messages << message
        end
      end
   end
   return @selected_messages
 end
 
end

 ## --------------------------- Command-Line Entry Point ----------------------------


class BuilderUI < Wx::App
    def on_init
        Builder.new()
    end
end

BuilderUI.new.main_loop
